﻿using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Xml;

namespace Matlus.MetaWeblog
{
  public class MetaWeblogManager
  {
    private IMetaWeblogProvider MetaWeblogProvider { get; set; }
    private Stream OutputStream { get; set; }
    private XmlDocument XmlDocument { get; set; }    

    public MetaWeblogManager(IMetaWeblogProvider metaWeblogProvider)
    {
      MetaWeblogProvider = metaWeblogProvider;
    }

    private string GetMethodName()
    {
      return XmlDocument.SelectSingleNode("//methodName").InnerText;
    }

    private string[] GetParams()
    {
      var valueNodes = XmlDocument.SelectNodes("//params/param/value[not(struct)]/*");
      var parameters = new string[valueNodes.Count];
      for (int i = 0; i < parameters.Length; i++)
        parameters[i] = valueNodes[i].InnerText;
      return parameters;
    }

    #region Response Generation Helper Methods
    
    private static XmlDocument CreateXmlDocument()
    {
      XmlDocument xmlDocument = new XmlDocument();
      var processingInstruction = xmlDocument.CreateProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\"");
      xmlDocument.AppendChild(processingInstruction);
      return xmlDocument;
    }

    private static void WriteStructCollectionToXmlNode<T>(XmlNode node, T[] collection)
    {
      var t = typeof(T);
      var propertyInfos = t.GetProperties();
      foreach (var item in collection)
      {
        var structNode = node.AddChild("value").AddChild("struct");
        WriteToStructNode(structNode, item, propertyInfos);
      }
    }

    private static void WriteStructToXmlNode<T>(XmlNode node, T obj)
    {
      var t = typeof(T);
      var propertyInfos = t.GetProperties();
      var structNode = node.AddChild("value").AddChild("struct");
      WriteToStructNode(structNode, obj, propertyInfos);
    }

    private static void WriteToStructNode<T>(XmlNode structNode, T obj, PropertyInfo[] propertyInfos)
    {
      foreach (var prop in propertyInfos)
      {
        var dataTypeNodeName = prop.PropertyType == typeof(Int32) ? "i4" : "string";
        var memberNode = structNode.AddChild("member");
        memberNode.AddChild("name").InnerText = prop.Name;
        var objValue = prop.GetValue(obj, null);        
        string value = null;

        if (prop.PropertyType == typeof(DateTime))
          value = ((DateTime)objValue).ToString("s");
        else if (prop.PropertyType == typeof(Boolean))
          value = (Boolean)objValue ? "1" : "0";
        else if (prop.PropertyType == typeof(String[]))
        {
          var dataNode = memberNode.AddChild("value").AddChild("array").AddChild("data");
          if (objValue == null) continue;
          foreach (var item in (string[])objValue)
            dataNode.AddChild("value").AddChild("string").InnerText = item;
          continue;
        }
        else
          value = objValue == null ? String.Empty : objValue.ToString();
        memberNode.AddChild("value").AddChild(dataTypeNodeName).InnerText = value;
      }
    }

    private static T DeserializeStruct<T>(XmlDocument xmlDocument) where T: new()
    {
      var memberNodes = xmlDocument.SelectNodes("//params/param/value/struct/member");
      var t = typeof(T);      
      var obj = new T();
      foreach (XmlNode node in memberNodes)
      {
        var nameNode = node.SelectSingleNode("./name");        
        var valueNode = node.SelectSingleNode("./value");
        var propInfo = t.GetProperty(nameNode.InnerText);
        if (propInfo == null) continue;
        if (valueNode.FirstChild.Name == "base64")
          propInfo.SetValue(obj, Convert.FromBase64String(valueNode.FirstChild.InnerText), BindingFlags.SetProperty, null, null, null);
        else if (valueNode.FirstChild.Name == "array")
        {
          /* Assuming only string arrays */
          var arrayElementNodes = valueNode.SelectNodes("array/data/value/string");
          var values = new string[arrayElementNodes.Count];
          for (int i = 0; i < values.Length; i++)
            values[i] = arrayElementNodes[i].InnerText;
          propInfo.SetValue(obj, values, BindingFlags.SetProperty, null, null, null);
        }
        else
        {
          if (propInfo.PropertyType == typeof(DateTime))
            propInfo.SetValue(obj, GetLocalDateTimeFromMetaWeblogFormat(valueNode.InnerText), BindingFlags.SetProperty, null, null, null);
          else if (propInfo.PropertyType == typeof(Boolean))
            propInfo.SetValue(obj, valueNode.InnerText == "0" ? false : true, BindingFlags.SetProperty, null, null, null);
          else
            propInfo.SetValue(obj, valueNode.InnerText, BindingFlags.SetProperty, null, null, null);
        }
      }
      return obj;
    }

    private static DateTime GetLocalDateTimeFromMetaWeblogFormat(string weblogDateStr)
    {
      if (weblogDateStr.Length == 17)
      {
        var dateParts = weblogDateStr.Split('T');
        var year = dateParts[0].Substring(0, 4);
        var month = dateParts[0].Substring(4, 2);
        var day = dateParts[0].Substring(6, 2);
        var formattedDateString = String.Format("{0}-{1}-{2}T{3}", year, month, day, dateParts[1]);

        return DateTime.Parse(formattedDateString, CultureInfo.CurrentCulture).ToLocalTime();
      }
      return DateTime.Now;
    }

    #endregion Response Generation Helper Methods

    #region Response Generation methods

    private static void WriteXmlRpcFault(Exception exception, Stream outputStream)
    {
      var xmlDocument = CreateXmlDocument();
      var faultNode = xmlDocument.AddChild("methodResponse").AddChild("fault");
      WriteStructToXmlNode(faultNode, new XmlRpcFaultInfo() { faultCode = 2041, faultString = exception.Message });
      xmlDocument.Save(outputStream);
    }

    private static void WriteStructCollectionToOutputStream<T>(Stream outputStream, T[] collection)
    {
      var xmlDocument = CreateXmlDocument();
      var dataNode = xmlDocument.AddChild("methodResponse").AddChild("params").AddChild("param").AddChild("value").AddChild("array").AddChild("data");
      WriteStructCollectionToXmlNode(dataNode, collection);
      xmlDocument.Save(outputStream);
    }

    private static void WriteStructToOutputStream<T>(Stream outputStream, T infoObj)
    {
      var xmlDocument = CreateXmlDocument();
      var paramNodeNode = xmlDocument.AddChild("methodResponse").AddChild("params").AddChild("param");
      WriteStructToXmlNode(paramNodeNode, infoObj);
      xmlDocument.Save(outputStream);
    }

    private static void WriteStringValueToOutputStream(Stream outputStream, string value)
    {
      var xmlDocument = CreateXmlDocument();
      var dataNode = xmlDocument.AddChild("methodResponse").AddChild("params").AddChild("param").AddChild("value").AddChild("string");
      dataNode.InnerText = value;
      xmlDocument.Save(outputStream);
    }

    private static void WriteBooleanValueToOutputStream(Stream outputStream, bool value)
    {
      var xmlDocument = CreateXmlDocument();
      var dataNode = xmlDocument.AddChild("methodResponse").AddChild("params").AddChild("param").AddChild("value").AddChild("boolean");
      dataNode.InnerText = value ? "1" : "0";
      xmlDocument.Save(outputStream);
    }

    private static void WriteInt32ValueToOutputStream(Stream outputStream, int value)
    {
      var xmlDocument = CreateXmlDocument();
      var dataNode = xmlDocument.AddChild("methodResponse").AddChild("params").AddChild("param").AddChild("value").AddChild("i4");
      dataNode.InnerText = value.ToString();
      xmlDocument.Save(outputStream);
    }

    #endregion Response Generation methods

    public void ProcessMetaWeblogApiCall(Stream inputStream, Stream outputStream)
    {
      XmlDocument = new XmlDocument();
      XmlDocument.Load(inputStream);
      OutputStream = outputStream;

      var methodName = GetMethodName();

      try
      {        
        switch (methodName)
        {
          case "blogger.getUsersBlogs":
            GetUsersBlogs();
            break;
          case "metaWeblog.getCategories":
          case "wp.getCategories":
            GetCategories();
            break;
          case "metaWeblog.getRecentPosts":
            GetRecentPosts();
            break;
          case "metaWeblog.newPost":
            NewPost();
            break;
          case "metaWeblog.editPost":
            EditPost();
            break;
          case "metaWeblog.getPost":
            GetPost();
            break;
          case "blogger.deletePost":
            DeletePost();
            break;
          case "metaWeblog.newMediaObject":
            NewMediaObject();
            break;
          case "blogger.getUserInfo":
            GetUserInfo();
            break;
        }

      }
      catch (Exception e)
      {
        WriteXmlRpcFault(e, outputStream);
      }      
    }

    #region MetaWeblog API Translation Methods    

    private void GetUsersBlogs()
    {
      var parameters = GetParams();
      var appkey = parameters[0];
      var userName = parameters[1];
      var password = parameters[2];

      BlogInfo[] userBlogs = MetaWeblogProvider.GetUserBlogs(appkey, userName, password);
      WriteStructCollectionToOutputStream(OutputStream, userBlogs);
    }

    private void GetCategories()
    {
      var parameters = GetParams();
      var userName = parameters[1];
      var password = parameters[2];

      CategoryInfo[] categories = MetaWeblogProvider.GetCategories(userName, password);
      WriteStructCollectionToOutputStream(OutputStream, categories);
    }

    private void GetRecentPosts()
    {
      var parameters = GetParams();
      var blogid = parameters[0];
      var userName = parameters[1];
      var password = parameters[2];
      var noOfPosts = int.Parse(parameters[3]);

      Post[] posts = MetaWeblogProvider.GetRecentPosts(userName, password, blogid, noOfPosts);
      WriteStructCollectionToOutputStream(OutputStream, posts);
    }

    private void NewPost()
    {
      var parameters = GetParams();
      var blogid = parameters[0];
      var userName = parameters[1];
      var password = parameters[2];
      var isPublished = parameters[3] == "0" ? false : true;
      var post = DeserializeStruct<Post>(XmlDocument);
      post.IsPublished = isPublished;

      int newPostId = MetaWeblogProvider.NewPost(userName, password, blogid, post);
      WriteInt32ValueToOutputStream(OutputStream, newPostId);
    }

    private void EditPost()
    {
      var parameters = GetParams();
      var userName = parameters[1];
      var password = parameters[2];
      var post = DeserializeStruct<Post>(XmlDocument);
      var isPublished = parameters[3] == "0" ? false : true;
      post.postid = int.Parse(parameters[0]);
      post.IsPublished = isPublished;

      MetaWeblogProvider.EditPost(userName, password, post);
      WriteBooleanValueToOutputStream(OutputStream, true);
    }

    private void GetPost()
    {
      var parameters = GetParams();
      var postid = parameters[0];
      var userName = parameters[1];
      var password = parameters[2];

      Post post = MetaWeblogProvider.GetPost(userName, password, int.Parse(postid));
      WriteStructToOutputStream(OutputStream, post);
    }

    private void DeletePost()
    {
      var parameters = GetParams();
      var postid = parameters[1];
      var userName = parameters[2];
      var password = parameters[3];

      MetaWeblogProvider.DeletePost(userName, password, int.Parse(postid));
      WriteBooleanValueToOutputStream(OutputStream, true);
    }

    private void NewMediaObject()
    {
      var parameters = GetParams();
      var postid = parameters[0];
      var userName = parameters[1];
      var password = parameters[2];

      var mediaObject = DeserializeStruct<MediaObject>(XmlDocument);
      var mediaObjectUrl = MetaWeblogProvider.NewMediaObject(userName, password, mediaObject);
      WriteStructToOutputStream(OutputStream, mediaObjectUrl);
    }

    private void GetUserInfo()
    {
      var parameters = GetParams();
    }

    #endregion MetaWeblog API Translation Methods
  }
}